M2.855 · Modelos avanzados de minería de datos
20221 - Máster universitario en Ciencias de datos (Data science)
Estudios de Informática, Multimedia y Telecomunicaciones
En esta práctica revisaremos y aplicaremos los conocimientos aprendidos en el módulo 1, donde nos centraremos en como aplicar diferentes técnicas para la carga y preparación de datos:
Consideraciones generales:
Formato de la entrega:
En la siguiente celda se deben cargar todas las librerías necesarias para la ejecución de la actividad. Se debe indicar y justificar el uso de librerías adicionales.
Nota: Actualizamos la librería scikit-learn para poder cargar el dataset sin normalizar (que se introdujo en la versión 1.1 de la librería).
!conda install -y scikit-learn=1.1
Collecting package metadata (current_repodata.json): ...working... done Solving environment: ...working... done # All requested packages already installed.
==> WARNING: A newer version of conda exists. <==
current version: 4.13.0
latest version: 22.9.0
Please update conda by running
$ conda update -n base -c defaults conda
# Librerías básicas
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn import datasets
from sklearn import preprocessing
from sklearn.manifold import TSNE
from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier
from sklearn.metrics import mean_absolute_error, mean_squared_error, mean_absolute_percentage_error, make_scorer, accuracy_score, roc_auc_score
from sklearn.model_selection import train_test_split, cross_val_score, cross_validate
from sklearn.model_selection import validation_curve
from sklearn.model_selection import GridSearchCV
import matplotlib.pyplot as plt
pd.set_option('display.max_columns', None)
seed = 100
%matplotlib inline
En primer lugar, deberéis cargar el conjunto de datos Diabetes dataset.
Este conjunto de datos se puede descargar de Internet o puede ser cargado directamente de la librería scikit-learn, que incorpora un conjunto de datasets muy conocidos y empleados para minería de datos y aprendizaje automático.
Cargad el conjunto de datos Diabetes y mostrad:
dataset = datasets.load_diabetes(as_frame=True, scaled=False)
X = dataset.data
y = dataset.target
print(type(X))
print(type(y))
<class 'pandas.core.frame.DataFrame'> <class 'pandas.core.series.Series'>
print(f'Número de elementos en el dataset: {X.shape[0]}')
print(f'Número de atributos totales: {X.shape[1]}')
print(f'Nombre de los atributos: {X.columns.values}')
Número de elementos en el dataset: 442 Número de atributos totales: 10 Nombre de los atributos: ['age' 'sex' 'bmi' 'bp' 's1' 's2' 's3' 's4' 's5' 's6']
Con el métrodo .info() podemos obtener la misma información de forma más cómoda.
X.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 442 entries, 0 to 441 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 age 442 non-null float64 1 sex 442 non-null float64 2 bmi 442 non-null float64 3 bp 442 non-null float64 4 s1 442 non-null float64 5 s2 442 non-null float64 6 s3 442 non-null float64 7 s4 442 non-null float64 8 s5 442 non-null float64 9 s6 442 non-null float64 dtypes: float64(10) memory usage: 34.7 KB
Con el mismo método .info() podemos ver cómo no hay nungun valor nulo en ningún atributo, ya que en todos ellos el número de no nulos es de 442 elementos. Lo mismo podríamos obtener de la siguiente forma, contando el número de NAs (missing values):
X.isna().sum()
age 0 sex 0 bmi 0 bp 0 s1 0 s2 0 s3 0 s4 0 s5 0 s6 0 dtype: int64
Realizad un análisis estadístico básico, siguiendo los criterios descritos a continución:
Notas:
pandas y sus funciones describe y value_counts, así como las funciones bar e hist de la librería matplotlib.X.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 442 entries, 0 to 441 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 age 442 non-null float64 1 sex 442 non-null float64 2 bmi 442 non-null float64 3 bp 442 non-null float64 4 s1 442 non-null float64 5 s2 442 non-null float64 6 s3 442 non-null float64 7 s4 442 non-null float64 8 s5 442 non-null float64 9 s6 442 non-null float64 dtypes: float64(10) memory usage: 34.7 KB
Según la descripción del dataset, los atributos son los siguientes:
Según la descripción, todos los atributos son numéricos a excepción del sexo, que sería categórico (binario). Sin embargo, en el dataframe aparece como flotante. Comprobamos los valores únicos de esta variable:
X['sex'].unique()
array([2., 1.])
Vemos como la variable sex sólo tiene dos valores posibles. Sin embargo, siendo estos 1 y 2 no tenemos idea a cuál de los dos sexos pertenece cada uno. Cambiamos el tipo de la variable a booleano True/False (sin saber a qué sexo corresponden estos valores, ya que no tenemos esa información).
X['sex'] = X['sex'] > 1.5
X.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 442 entries, 0 to 441 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 age 442 non-null float64 1 sex 442 non-null bool 2 bmi 442 non-null float64 3 bp 442 non-null float64 4 s1 442 non-null float64 5 s2 442 non-null float64 6 s3 442 non-null float64 7 s4 442 non-null float64 8 s5 442 non-null float64 9 s6 442 non-null float64 dtypes: bool(1), float64(9) memory usage: 31.6 KB
C:\Users\gvillalba\AppData\Local\Temp\ipykernel_14448\3526253516.py:1: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy X['sex'] = X['sex'] > 1.5
Comprobamos la frecuencia de la variable categórica sex:
X['sex'].value_counts()
False 235 True 207 Name: sex, dtype: int64
Hacemos un gráfico de barras para mostrar la misma información:
# Set Seaborn aesthetics
# Default matplotlib params
plt.rcParams["figure.figsize"] = (6,4)
plt.rcParams['lines.linewidth'] = 2
plt.style.use('ggplot')
ax = sns.countplot(x='sex', data=X)
ax.set_title('Sex distribution', fontsize=16, loc='left')
ax.set_ylabel('Number of samples')
ax.set_xlabel('Sex')
ax.set_xticklabels(['Sex1', 'Sex2'])
plt.show()
Vemos las estadísticas básicas de las variables numéricas:
X.describe()
| age | bmi | bp | s1 | s2 | s3 | s4 | s5 | s6 | |
|---|---|---|---|---|---|---|---|---|---|
| count | 442.000000 | 442.000000 | 442.000000 | 442.000000 | 442.000000 | 442.000000 | 442.000000 | 442.000000 | 442.000000 |
| mean | 48.518100 | 26.375792 | 94.647014 | 189.140271 | 115.439140 | 49.788462 | 4.070249 | 4.641411 | 91.260181 |
| std | 13.109028 | 4.418122 | 13.831283 | 34.608052 | 30.413081 | 12.934202 | 1.290450 | 0.522391 | 11.496335 |
| min | 19.000000 | 18.000000 | 62.000000 | 97.000000 | 41.600000 | 22.000000 | 2.000000 | 3.258100 | 58.000000 |
| 25% | 38.250000 | 23.200000 | 84.000000 | 164.250000 | 96.050000 | 40.250000 | 3.000000 | 4.276700 | 83.250000 |
| 50% | 50.000000 | 25.700000 | 93.000000 | 186.000000 | 113.000000 | 48.000000 | 4.000000 | 4.620050 | 91.000000 |
| 75% | 59.000000 | 29.275000 | 105.000000 | 209.750000 | 134.500000 | 57.750000 | 5.000000 | 4.997200 | 98.000000 |
| max | 79.000000 | 42.200000 | 133.000000 | 301.000000 | 242.400000 | 99.000000 | 9.090000 | 6.107000 | 124.000000 |
De estas estadísticas básicas podemos concluir que los atributos tienen escalas de valores muy distintas.
Realizamos a continuación el histograma de todas las variables numéricas.
fig, axes = plt.subplots(3,4, figsize=(14,7))
for ax in axes.flatten():
ax.set_axis_off()
for feat, ax in zip(X.columns.drop('sex'), axes.flatten()):
sns.histplot(data=X[feat], bins=20, kde=True, stat="percent", ax=ax)
ax.set_title(feat.title(), loc='left')
ax.set_xlabel('')
ax.set_ylabel('')
ax.set_axis_on()
fig.suptitle('Variables distribution', fontsize=18)
fig.tight_layout()
Comentad los resultados obtenidos.
De la variable sex podemos concluir que se trata de una variable categórica bastante balanceada, pues no hay grandes diferencias en el número de muestras presentes de ambas categorías.
En cuanto a las variables numéricas, se puede observar como tienen un rango de valores posibles muy diferente. Además, viendo el histograma podemos decir como la mayoría de ellas (a excepción de s4) siguen una distribución que no dista mucho de la normal, si bien alguna de ellas tienen las colas cortadas (bmi, age).
En este ejercicio exploraremos la relación de los atributos con la variable respuesta, mediante gráficos y analizando las correlaciones de los atributos numéricos.
response = "target"
cat_feats = "sex"
num_feats = ['age', 'bmi', 'bp', 's1', 's2', 's3', 's4', 's5', 's6']
print('Respuesta (target) :', response)
print("Atributos categóricos :", cat_feats)
print("Atributos numéricos :", num_feats)
Respuesta (target) : target Atributos categóricos : sex Atributos numéricos : ['age', 'bmi', 'bp', 's1', 's2', 's3', 's4', 's5', 's6']
Calculad y mostrad la correlación entre todos los atributos numéricos y la variable de respuesta (o variable objetivo).
complete_df = pd.concat([X, y], axis=1)
plt.figure(figsize=(8,7))
corr_matrix = complete_df.drop(cat_feats, axis=1).corr()
ax = sns.heatmap(corr_matrix, center=0, linewidths=2, cmap="vlag", annot=True)
ax.set_title('Correlation matrix')
plt.show()
Representad gráficamente las relaciones entre todas las parejas de las variables numéricas (sin incluir la variable respuesta) del conjunto de datos.
La finalidad de este ejercicio es poder observar y analizar las correlaciones de manera gráfica entre los pares de variables.
Notas:
pairplot de la librería seaborn.sns.pairplot(X[num_feats], diag_kind="hist", kind='reg', corner=True, plot_kws={'scatter_kws': {'alpha': 0.3}, 'line_kws': {'color':'#348ABD'}})
plt.show()
Podemos ver como hay variables independientes con gran correlación entre ellas, como ocurre con s1 y s2, lo cual nos indica que podríamos realizar alguna tranformación linal y ser capaces de reducir la dimensionalidad sin perder información. Además, tener variables de entrada fuertemente correladas nos puede causar problemas de multicolinealidad.
Identificad los 2 atributos que tienen una correlación más fuerte con la variable de respuesta, y los 2 con una correlación más débil (considerando el coeficiente de correlación mayor o menor en valor absoluto).
Para observar y analizar las correlaciones gráficamente, representad, para cada uno de los 4 atributos identificados, un scatter plot con el atributo en el eje X y la respuesta en el eje Y. Además, en cada gráfico añadid la representación de una regresión lineal que aproxime los puntos.
Notas:
regplot de la librería seaborn.Identificamos las dos variables con mayor y menor correlación en valor absuluto con la variable dependiente. También nos podríamos fijar en la matriz de correlaciones generada anteriormente.
two_largest = corr_matrix['target'].drop('target').apply(abs).nlargest(2).index.to_list()
two_smallest = corr_matrix['target'].drop('target').apply(abs).nsmallest(2).index.to_list()
print('Two largest correlations with target are: ', two_largest)
print('Two smallest correlations with target are: ', two_smallest)
Two largest correlations with target are: ['bmi', 's5'] Two smallest correlations with target are: ['s2', 'age']
Vemos cómo los valores absolutos de correlación mayores con la variable objetivo target están con bmi y s5. De igual manera, las correlaciones menores en valor absoluto se dan con s2 y age. En este caso todas ellas tienen correlación positiva con la variable dependiente target. Representamos estas variables frente a la variable target con su recta de regresión:
fig, axes = plt.subplots(2, 2, figsize=(10,8), sharey=True)
for feat, ax in zip(two_largest+two_smallest, axes.flatten()):
sns.regplot(x=feat, y='target', data=complete_df, scatter_kws={'alpha':0.4, 'color':'#348ABD'}, ax=ax)
ax.set_title(f'Target vs. {feat}')
ax.set_ylabel('')
ax.set_xlabel('')
fig.tight_layout()
plt.show()
Observando los gráficos, comentad brevemente si conseguís ver las altas o bajas correlaciones que habíais identificado numéricamente.
Efectivamente, se puede ver claramente cómo los dos atributos con una mayor correlación con la variable dependiente, bmi y s5, tienen una pendiente positiva bastante fuerte cuando las representemos frente a la variable objetivo en un scatter plot. La recta de regresión por supuesto nos dice la mismo, con una pendiente considerable.
Por el contrario, en los dos atributos con menor correlación en valor absoluto, age y s2, es más complicado ver una pendiente clara cuando los representamos frente a la variable objetivo, y en este caso la recta de regresión sale mucho más plana.
En este ejercicio se aplicarán métodos de reducción de la dimensionalidad al conjunto original de datos. El objetivo es reducir el conjunto de atributos a un nuevo conjunto con menos dimensiones, pero que contengan la máxima información posible presente en los atributos originales.
Relizad las siguientes acciones:
Aplicad el método de reducción de la dimensionalidad Principal Component Analysis (PCA) para reducir a 2 dimensiones el dataset original que contiene todos los atributos.
Generad un gráfico con el resultado del PCA en el que se muestre, en función de los valores de las dos componentes en los ejes X e Y, el valor de la respuesta (target) usando la escala de colores. El objetivo es visualizar la variación del atributo objetivo en función de los valores de las componentes principales generadas.
Nota:
scikit-learn.matplotlib con el parámetro c, que indica el color de los puntos, igual a la variable objetivo para generar el gráfico.Como hemos cargado el dataset sin normalizar y por lo tanto tenemos escalas muy diferentes en los atributos, no podemos aplicar directamente PCA. Esto es así porque las diferentes escalas de datos presentes sesgarían el algoritmo para dar mayor peso a aquellas variables con mayor rango, lo que invalidaría su resultado. Aplicamos por lo tanto una estandarización de los datos previa al analisis de componentes principales. Para ello, importamos Pipeline y StandardScaler de la misma librería Scikit-Learn.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
n_pca = 2
pipeline = Pipeline([('scaling', StandardScaler()), ('pca', PCA(n_components=n_pca))])
pca_comp = pipeline.fit_transform(X)
pca_dict = {f'pca{i+1}':pca_comp[:,i] for i in range(n_pca)}
pca_dict['target'] = y
pca_df = pd.DataFrame(pca_dict)
pca_df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 442 entries, 0 to 441 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 pca1 442 non-null float64 1 pca2 442 non-null float64 2 target 442 non-null float64 dtypes: float64(3) memory usage: 10.5 KB
A partir de la transformación PCA, podemos comprobar también el porcentaje de varianza de los datos originales que se conserva en los datos tras realizar la tranformación y quedarnos sólo con las dos primeras componentes.
pca_var = pipeline['pca'].explained_variance_ratio_
fig, ax = plt.subplots(figsize=(6,4))
ax.bar(x=range(n_pca), height=pca_var)
ax.set_xlabel('PCA component')
ax.set_ylabel('Explained variance ratio')
ax.set_title('PCA Explained variance ratio', loc='left')
ax.set_xticks(list(range(n_pca)))
ax.set_xticklabels(pca_df.columns.drop('target'))
for i, v in enumerate(pca_var):
ax.text(i-0.05, v+0.003, f'{v:.2f}')
plt.show()
Podemos ver cómo sólamente con la primer componente ya explicamos el 40% de la varianza original en los datos, un valor bastante alto. Con las dos primeras componentes que nos quedamos, podemos explicar el 55% de la varianza original, por lo que retenemos más del 50% de la información con sólamente 2 variables en lugar de las 9 iniciales. El haber reducido a sólo dos variables nos permite dibujar los puntos en un gráfico, que hacemos a continuación.
palette = sns.color_palette("YlOrBr", as_cmap=True)
ax = sns.scatterplot(x='pca1', y='pca2', hue='target', data=pca_df, palette=palette)
ax.set_title('First 2 PCA components vs. Target', loc='left')
plt.show()
Podemos ver claramente como estas dos primeras componentes, y en especial la primera, determina bastante bien el valor de la variable objetivo target (la correlación es alta), ya que la mayoría de los valores altos de esta variable se concentran a la derecha del gráfico, donde pca1 es alto.
Relizad las siguientes operaciones:
Nota:
scikit-learn.learning_rate y perplexity.matplotlib con el parámetro c, que indica el color de los puntos, igual a la variable objetivo para generar el gráfico.import warnings
warnings.filterwarnings('ignore')
n_comp = 2
tsne_pipe = Pipeline([('scaling', StandardScaler()), ('tsne', TSNE(n_components=n_comp, perplexity=50, random_state=seed))])
tsne_comp = tsne_pipe.fit_transform(X)
tsne_dict = {f'comp{i+1}':tsne_comp[:,i] for i in range(n_comp)}
tsne_dict['target'] = y
tsne_df = pd.DataFrame(tsne_dict)
tsne_df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 442 entries, 0 to 441 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 comp1 442 non-null float32 1 comp2 442 non-null float32 2 target 442 non-null float64 dtypes: float32(2), float64(1) memory usage: 7.0 KB
ax = sns.scatterplot(x='comp1', y='comp2', hue='target', data=tsne_df, palette=palette)
ax.set_title('t-SNE components vs. Target', loc='left')
plt.show()
Realizamos a continuación un paramétrico variando los parámetros perplexity y learning_rate de t-SNE entre un arango de valores razonable dentro de los habituales.
parameters = {'perplexity' : np.arange(5,51,10),
'learning_rate' : np.arange(200,1001,200)
}
def test_tsne(X: pd.DataFrame, y: pd.Series, parameters: dict):
combinations = np.array(np.meshgrid(*[mat for mat in parameters.values()])).T.reshape(-1,2)
params_list = [dict(zip(parameters.keys(),comb)) for comb in combinations]
n_cols=5
n_rows=len(combinations) // n_cols if len(combinations) % n_cols == 0 else len(combinations) // n_cols +1
print(f'Creating {n_rows} x {n_cols} grid to plot {len(combinations)} combinations.')
X_norm = StandardScaler().fit_transform(X)
fig, axes = plt.subplots(n_cols, n_rows, figsize=(2.5*n_cols,2.5*n_rows))
for param, ax in zip(params_list, axes.flatten()):
tsne = TSNE(n_components=2, random_state=seed, **param)
tsne_comp = tsne.fit_transform(X_norm)
tsne_dict = {f'comp{i+1}':tsne_comp[:,i] for i in range(n_comp)}
tsne_df = pd.DataFrame(tsne_dict)
tsne_df['target'] = y
sns.scatterplot(x='comp1', y='comp2', hue='target', data=tsne_df, legend=False, palette=palette, ax=ax)
ax.set_title(', '.join([f'{k}: {v}' for k, v in param.items()]), fontsize=9)
ax.set_xlabel('')
ax.set_ylabel('')
ax.set_xticklabels('')
ax.set_yticklabels('')
fig.suptitle(f't-SNE parameter tuning')
fig.tight_layout()
plt.show()
test_tsne(X, y, parameters)
Creating 5 x 5 grid to plot 25 combinations.
Observando los dos gráficos, responded a las siguinetes preguntas:
La reducción de la dimensionalidad ha sido correcta para poder visualizar los datos en 2D, y en el caso de PCA vemos que nos hemos quedado con una varianza original decente (55%). Los resultados que se obtienen con PCA y t-SNE son muy distintos porque aunque en ambos el objetivo es reducir la dimensionalidad de los datos existentes, ambos lo llevan a cabo de forma muy diferente. PCA realiza una tranformación lineal del espacio n-dimensional de los datos originales, de forma que las dimensiones resultantes estén ordenadas en orden decreciente con respecto la variabilidad presente en los datos en dicha dimensión. De esta forma, truncando el número de dimensiones (en este caso a 2), nos quedamos con las dimensiones que mayor variabilidad representan en el conjunto de datos. Un problema importante es que por ser un algoritmo lineal, no capta las simulitudes no lineales entre estos y al representar los datos en un espacio de menor dimensionalidad, se tiende a preservar la estructura global original de los datos, que en ocasiones puede no ser una interpretación correcta. Las mayores ventajas de PCA son que es un algoritmo muy sencillo de implementar e intuitivo y que no requiere de ningún parámetro a excepción del número de componentes finales con el que nos quedamos.
t-SNE, sin embargo, sigue un método estadístico basado en la distribución t de Student para representar las distancias de todos los puntos entre ellos. El algoritmo para llevar a cabo esta tranformación es más complejo que PCA, pero tiene como principal ventaja que pueden captar las similitudes no lineales entre distintos puntos, y no sólamente las lineales como PCA, permitiendo una mucho más clara representación de los datos en el espacio de dimensionalidad reducida para casos en los que esta relación no sea lineal.
En el caso concreto que nos ocupa, ambos métodos nos ofrecen una calidad de visualización aceptable, ya que la relación que tenemos es bastante lineal (hemos visto correlaciones considerables entre variables independientes y variable dependiente). Con t-SNE vemos cómo los valores con target más alta tienden a concentrarse en una misma zona en el espacio de dos dimensiones, pero algo similar ocurría con PCA. Con t-SNE vemos además que variando los parámetros perplexity y rearning_rate la representación que obtenemos de los mismos datos es muy diferente, pero en todos los casos tenemos los puntos con valor target alto en una misma zona espacial. Por lo tanto, en base a los resultados, diría que en este caso ambos métodos funcionarían de forma similar si los utilizásemos para reducir la dimensionalidad de los atributos de entrada a 2 y utilizarlos como variables predictoras de la variable dependiente.
Por otro lado, es curioso el hecho de que el s-SNE presenta una estructura de los puntos en dos clusters en la mayoría de los casos, algo que lleva a pensar que hay una estructura interna en los datos que permitiría dividirlos en dos grupos claramente diferenciados si utilizásemos un algoritmo no supervisado de clustering. Comprobamos que estos dos clusters se corresponden, precisamente, a ambos sexos presentes en los datos, como podemos comprobar, coloreando por sexo el caso de t-SNE con parámetros por defecto:
tsne_df['sex'] = X['sex']
ax = sns.scatterplot(x='comp1', y='comp2', hue='sex', data=tsne_df)
ax.set_title('t-SNE components vs. Sex', loc='left')
plt.show()
En este último ejercicio se trata de aplicar un método de aprendizaje supervisado, concretamente el Random Forest para regresión, para predecir el valor objetivo (target) y evaluar la precisión obtenida con el modelo.
Para eso usaremos el conjunto de datos original con todos los atributos menos la variable dependiente (target).
Usando el conjunto de datos original:
n_estimators=10 para mantener el modelo simple y random_state=seed).cv=5 ya es suficiente).Notas:
RandomForestRegressor de sklearn.cross_val_score de sklearn, y modificar su parámetro scoring si fuese necesario.Nota: en un caso más habitual, habríamos apartado un conjunto de datos como evaluación. Sin embargo, en este caso no estamos realizando evaluación de diferentes modelos, sino sólamente comprobando el comportamiento de uno de ellos. Por otro lado, al hacer cross validation no necesitamos apartar un conjunto de test. Por lo tanto, no apartamos un conjunto de evaluación desde el principo, porque en este caso concreto no es necesario, y así aprovechamos el mayor número posible de datos para entrenar. Lo mismo sucede en el apartado siguiente de clasificación.
Nota 2: utilizamos cross_validate en lugar de cross_val_score porque la primera permite el uso directamente de varios scores en lugar de uno sólo. De esta forma, no tenemos que entrenar el modelo repetidas veces para evaluarlo con las diferentes métricas, sino que se entrena una vez y se calculan todas ellas. Además tiene la ventaja adicional de poder devolver también las métricas obtenidas en cada conjuto de entrenamiento. Lo mismo hacemos en el apartado siguiente de clasificación.
reg_score_types = ['neg_mean_absolute_error', 'neg_mean_squared_error', 'neg_mean_absolute_percentage_error']
rf_reg = RandomForestRegressor(n_estimators=10, random_state=seed)
reg_scores = cross_validate(rf_reg, X, y, scoring=reg_score_types, return_train_score=True, cv=5, n_jobs=-1)
def show_mean_std_scores(scores):
df = pd.DataFrame(scores)
df.columns = [col.replace('_neg', '') for col in df.columns]
useful_cols = [col for col in df.columns if 'test' in col or 'train' in col]
return df[useful_cols].agg(['mean', 'std']).apply(abs).T
show_mean_std_scores(reg_scores)
| mean | std | |
|---|---|---|
| test_mean_absolute_error | 48.291877 | 3.387339 |
| train_mean_absolute_error | 19.424356 | 0.592028 |
| test_mean_squared_error | 3600.290209 | 446.802257 |
| train_mean_squared_error | 682.783288 | 42.030678 |
| test_mean_absolute_percentage_error | 0.421703 | 0.047291 |
| train_mean_absolute_percentage_error | 0.166257 | 0.008047 |
¿A qué se deben las diferencias numéricas entre las distintas métricas?
¿Qué muestras y errores crees que pueden influir más o menos en el incremento/decremento de las distintas métricas?
Podemos extraer varias conclusiones de los resultados:
Para este apartado se usará el conjunto de datos original pero como target (y) la variable "sex" (binaria, con valores 0 y 1), transformando el problema de regresión a clasificación.
Genera el conjunto de variables independientes X con los datos originales pero sin la variable target ni sex (será la nueva variable dependiente).
Genera la variable dependiente y como un array de tipo int que contenga el sexo asociado a cada fila para ser predicho.
Generamos los nuevos dataframes con las variables dependientes y objetivo. La variable objetivo la dejamos tal y como la teníamos porque ya la habíamos cambiado a tipo booleano anteriormente.
Como ya hemos visto que la variable sex está balanceada, al convertirla ahora en variable objetivo no tendremos problema por desbalanceo de clases.
X_clas = X.drop('sex', axis=1)
y_clas = X['sex']
y_clas.info()
<class 'pandas.core.series.Series'> RangeIndex: 442 entries, 0 to 441 Series name: sex Non-Null Count Dtype -------------- ----- 442 non-null bool dtypes: bool(1) memory usage: 570.0 bytes
n_estimators=10 para mantener el modelo simple y random_state=seed).cv=5 ya es suficiente).Notas:
RandomForestClassifier de sklearn.cross_val_score de sklearn, y modificar su parámetro scoring si fuese necesario.clf_score_types = ['accuracy', 'roc_auc']
rf_clf = RandomForestClassifier(n_estimators=10, random_state=seed)
rf_score = cross_validate(rf_clf, X_clas, y_clas, scoring=clf_score_types, return_train_score=True, cv=5, n_jobs=-1)
show_mean_std_scores(rf_score)
| mean | std | |
|---|---|---|
| test_accuracy | 0.640220 | 0.026578 |
| train_accuracy | 0.982463 | 0.007327 |
| test_roc_auc | 0.701663 | 0.043937 |
| train_roc_auc | 0.999496 | 0.000249 |
De los resultados podemos concluir que el modelo está sobreajustanto (overfitting) los datos de entrenamiento fuertemente, ya que las métricas en el conjunto de entrenamiento ofrecen un comportamiento mucho mejor (y casi perfecto) que en el conjunto de test.
Si los valores de la columna sex estuviesen desbalanceados con un 99% de ceros y un 1% de unos.
Para las dos métricas anteriores, ¿qué score obtendríamos con un modelo que siempre indicase 0?
La exactidud obtenida en ese caso sería del 99% (99% de aciertos con el modelo dummy), un dato que puede parecer en principio bueno, pero que realmente sólo lo es por el desbalanceo de las clases objetivo.
Por otro lado, la ROC AUC es el área que queda bajo la curva ROC, que a su vez se define como TPR (true positive rate) vs. FPR (false positive rate), teniendo en cuenta que podemos mover el umbral de decisión de clase positiva. Sin embaro, como no tenemos un umbral de decisión que mover al clasificar siempre como 0, no podemos calcular la curva como tal, sino sólamente un punto.
$TPR = \frac{TP}{P} = \frac{99}{99} = 1$
$FPR = \frac{FP}{N} = \frac{1}{1} = 1$
Por lo tanto el punto donde se encontraría este clasificador fijo sería el (1, 1) dentro de la curva ROC. En este caso por lo tanto si miramos el TPR tenemos un score perfecto (todas las clases positivas se clasifican como tal), pero el FPR nos da un score muy malo (todas los elementos negativos se clasifican como positivos). Mirando la curva ROC por lo tanto tenemos una visión mucho más ajustada a la realidad de la bonanza de un clasificador cuando las clases están desbalanceadas que mirando la exactitud.
Para calcular la curva completa ROC debaríamos tener un clasificador que nos devuelva probabilidades y así mover el umbral de decisión entre clases, cosa que no tenemos. Si tenemos la curva ROC, además podemos calcular el área bajo su curva (ROC AUC), que se encontrará entre 0.5 (clasificador dummy, con curva ROC que es una recta de pendiente unidad) y 1 (clasificador perfecto).
He reutilizado y adaptado código de dos notebooks propios de Kaggle:
https://www.kaggle.com/code/gersonvillalba/wine-eda-quality-prediction
https://www.kaggle.com/code/gersonvillalba/spaceship-titanic-eda-full-pipeline-model
Además he consultado la siguiente documentación externa:
https://scikit-learn.org/stable/datasets/toy_dataset.html#diabetes-dataset
https://matplotlib.org/3.1.1/api/pyplot_summary.html
https://seaborn.pydata.org/index.html
https://stackoverflow.com/questions/44980658/remove-the-extra-plot-in-the-matplotlib-subplot
https://www.themachinelearners.com/tsne/